5.6 Referenz- und Wertetypen
 
Das .NET-Typsystem setzt sich aus zwei Subsystemen zusammen:
|
Wertetypen |
|
Referenztypen |
Zu den Wertetypen werden primitive Datentypen wie int und long sowie die in den beiden vorhergehenden Abschnitten behandelten Strukturen und Enumerationen gezählt, zu den Referenztypen der Typ string, alle Arrays und – ganz allgemein ausgedrückt – alle klassenbasierten Objekte. Obwohl es im ersten Augenblick den Anschein haben mag, dass hinter Wertetypen nur »normale« Dateninformationen stehen, werden auch diese als Objekte angesehen und hinter den Kulissen der .NET-Laufzeit als solche behandelt. Der Unterschied zwischen Referenz- und Wertetypen ist vielmehr in der Allokierung des Systemspeichers zu finden.
Eine Variable, die einen Wertetyp repräsentiert, allokiert auf dem Stack Speicher für die Daten. Der Stack ist im RAM angesiedelt, wird aber vom Prozessor durch einen so genannten Stack Pointer direkt unterstützt. Dieser ist in der Lage, auf dem Stack neuen Speicher zu reservieren, kann ihn aber auch freigeben. Dieses Verfahren ist sehr effizient und schneller als das Allokieren von Speicher im Heap für Referenztypen.
Als Heap wird der Speicher im RAM bezeichnet, der allgemeinen Zwecken zur Verfügung steht. Dazu gehören auch die Zustandsdaten eines Objekts. Der Compiler weiß nicht, wie viel Speicher er zur Laufzeit eines Programms im Heap allokieren muss und wie lange die Allokierung aufrechterhalten bleibt. Das garantiert natürlich ein hohes Maß an Flexibilität, da zur Laufzeit eine zur Entwicklungszeit nicht vorhersehbare Anzahl von Objekten erzeugt werden kann. Die Flexibilität gibt es jedoch nicht kostenlos: Es dauert etwas länger, auf dem Heap Speicher zu reservieren.
Wird mit
eine int-Variable deklariert, schreibt sich der 32-Bit-Wert in den Stack. Beachten Sie bitte, dass bei einem Wertetyp der new-Operator zur Initialisierung nicht angegeben werden muss – bei einem Referenztyp ist das eine unabdingbare Forderung, denn erst mit
| Circle kreis = new Circle();
|
wird auf dem Heap ein Speicherbereich allokiert und initialisiert, auf den danach die Referenz kreis zeigt.
Ein daraus folgendes, wichtiges Unterscheidungsmerkmal zwischen Referenz- und Wertetypen ist, dass Wertetypen niemals den Inhalt null haben können. Wenn mit
eine auf einer Klasse basierende Objektvariable deklariert wird, ohne mit new initialisiert zu werden, ist deren Inhalt null.
Bemerkenswert ist die unterschiedliche Wirkungsweise des Zuweisungsoperators zwischen einem Werte- und einem Referenztyp. Betrachten Sie dazu zunächst das Codefragment eines Wertetypen:
| long lngVar = 64;
|
| long lngSecond = lngVar;
|
Nach der Ausführung des Codes existieren zwei Variablen vom Typ long, die denselben Inhalt haben. long wird, wie nahezu alle elementaren Datentypen, von der Common Language Runtime als Wertetyp angesehen und entsprechend behandelt. Die Änderung des Inhalts der Variablen lngVar wird sich nicht auf den Inhalt der Variablen lngSecond auswirken, weil zwischen den beiden keine Verbindung existiert.
Nun soll ein ähnliches Codefragment betrachtet werden, diesmal allerdings auf Basis eines Referenztyps.
| Circle kreis1 = new Circle();
|
| Circle kreis2 = new Circle();
|
| kreis1.Radius = 320;
|
| kreis2 = kreis1;
|
| Console.WriteLine(kreis2.Radius);
|
In diesem Programmausschnitt werden zwei Objekte gleichen Typs erzeugt. Anschließend wird die Referenz, die auf das zweite Objekt im Speicher zeigt, auf die Adresse des ersten umgebogen. Die Ausgabe an der Konsole (= 320) beweist, dass beide Objektvariablen mit demselben Objekt im Speicher operieren. Der Zuweisungsoperator, angewandt auf Referenztypen, bewirkt demnach ein Verbiegen des Zeigers, bei Wertetypen einen Kopiervorgang.
5.6.1 Typumwandlung mit Boxing
 
Die Ausführungen des vorhergehenden Abschnitts scheinen im Widerspruch zu einer anderen Aussage zu stehen, die vorher schon mehrfach getroffen worden ist: Die Common Language Runtime betrachtet alles als Objekt. Wie kann aber ein Wertetyp, der definitionsgemäß Speicher im Stack und nicht auf dem Heap allokiert, als Objekt angesehen werden?
Die Antwort auf die Frage lautet: mittels eines Verfahrens, das Boxing genannt wird. Durch diese Technik wird ein Objekt erst dann zu einem solchen, wenn es auch wirklich als Objekt benötigt wird. Betrachten Sie dazu das folgende Codefragment:
| class ClassA {
|
| public enum Spielkarte {
|
| Karo = 9,
|
| Herz,
|
| Pik,
|
| Kreuz
|
| }
|
| static void Main(string[] arr) {
|
| Spielkarte myGame;
|
| myGame = Spielkarte.Karo;
|
| Console.WriteLine("Wert = {0}", myGame);
|
| }
|
| }
|
Die Ausführung des Programms wird an der Konsole die Ausgabe
haben. Wir müssen damit auch feststellen, dass der Typ Spielkarte von der Methode WriteLine erkannt wird und zu einer korrekten Ausgabe führt. Sie können in der .NET-Dokumentation nachschlagen, selbstverständlich werden Sie keine passende Parameterliste der überladenen WriteLine-Methode finden, die den Typ Spielkarte angibt.
Die Technologie, die sich hinter den Kulissen der .NET-Laufzeitumgebung abspielt und zu der gewünschten Ausgabe führt, ist das Boxing. Dabei wird ein Wertetyp – und um einen solchen handelt es sich bei einer Auflistung – implizit in den Typ Object, also einen Referenztyp, konvertiert. Diese Technik wird immer dann automatisch eingesetzt, wenn ein Wertetyp verwendet wird, wo Object erforderlich wäre.
Das hört sich sehr kompliziert an, deshalb wollen wir die einzelnen Schritte anhand des Beispielcodes von oben nachvollziehen. Beim Aufruf von
| Console.WriteLine("Wert = {0}", myGame);
|
sucht der Compiler nach einer passenden Überladung der Methode WriteLine. Er findet eine adäquate in:
| public static void WriteLine(String, Object);
|
Der zweite Parameter ist vom Typ Object, bekommt aber eine Variable des Typs Spielkarte übergeben. Da Enumerationen von der Klasse Object abgeleitet werden, wird implizit eine Object-Referenz erzeugt und auf den Stack gelegt:
Diese Referenz verweist auf ein neues Objekt im Heap, das die kopierten Werte der auf dem Stack befindlichen Variablen myGame unter Angabe des Typs in einer Art Box enthält. Als Resultat dieser Operation liegen auf dem Stack zwei Wertetypen:
1.
das Original des Wertetyps (also myGame)
| 2. |
die Referenz auf das Object-Objekt |
|
|
|
Der neue Verweis wird an die Methode WriteLine übergeben, die damit ihrerseits wieder den Inhalt auswerten kann. Die folgende Abbildung macht den Sachverhalt deutlich, dass beim Boxing die Kopie eine Wertetyps in einen Referenztyp erfolgt, damit also zwei Elemente desselben Inhalts vorliegen.
Wenn die Aussage stimmt, dass beim Boxing die Kopie eines Wertetyps erstellt wird, muss sich auch der Beweis führen lassen. Dazu wird nach der Erstellung der Kopie der Inhalt des Referenz- oder des Wertetyps verändert:
| static void Main(string[] arr) {
|
| int intVar = 100;
|
| Object intObj = intVar;
|
| intVar = 4711;
|
| Console.WriteLine("Der Inhalt von intObj = {0}", intObj);
|
| Console.WriteLine("Der Inhalt von intVar = {0}", intVar);
|
| Console.Read();
|
| }
|
 Hier klicken, um das Bild zu vergrößern
Abbildung 5.1 Boxing eines Wertetypen
Zuerst wird die int-Variable intVar deklariert und ihr ein Wert zugewiesen. Anschließend wird intVar in einen Object-Typ konvertiert. In der dritten Anweisung wird intVar ein anderer Wert zugewiesen. Wenn die geboxte Variable unabhängig von der auf dem Stack ist, müssen an der Konsole zwei unterschiedliche Ausgaben erfolgen. Dies wird durch
| Der Inhalt von intObj = 100
|
| Der Inhalt von intVar = 4711
|
tatsächlich bestätigt.
5.6.2 Die Unboxing-Konvertierung
 
Die Konvertierung eines Object-Typs in einen Wertetyp ist ein Rückführungsprozess und wird als Unboxing bezeichnet. Die .NET-Laufzeitumgebung prüft dabei zuerst, ob es sich wirklich um einen »geboxten« Wert eines bestimmten Typs handelt. Danach wird der Inhalt des Referenztyps in die Variable eines Wertetyps kopiert.
| int intVar = 4711;
|
| Object obj = intVar;
|
| obj = 15;
|
| // Unboxing
|
| intVar = Convert.ToInt32(obj);
|
Natürlich gelten auch hier die bekannten Regeln der Konvertierung.
5.6.3 Zusammenfassung
 
|
Enumerationen sind Aufzählungen von Konstanten gleichen Datentyps und daher zur Laufzeit nicht veränderlich. |
|
Wird einer Enumeration kein Datentyp zugeordnet, ist die Enumeration standardmäßig vom Typ int. |
|
Die Wertzuweisung an enum-Konstanten ist optional. Wird darauf verzichtet, steht das erste Element für den Wert 0, der sich mit jedem Folgeelement um +1 erhöht. Die einzelnen Elemente einer Enumeration werden voneinander durch ein Komma getrennt. |
|
Die Common Language Runtime unterscheidet Werte- und Referenztypen. Referenztypen sind alle auf Klassen basierenden Objekte sowie Strings und Arrays. Zu den Wertetypen zählen die elementaren Datentypen (int, float usw.), außerdem auch noch Strukturen und enum-Konstantenaufzählungen. |
|
Wertetypen reservieren auf dem Stack Speicher, während Referenztypen ihren Speicherbedarf im Heap allokieren. |
|
Eine Variable, die auf einem Referenztyp basiert, wird grundsätzlich mit dem new-Operator initialisiert und kann den Inhalt null haben. |
|
Von der Laufzeit werden Referenz- und Wertetypen gleich behandelt, da die Common Language Runtime alles als Objekt behandelt. Um einen Wertetyp zur Laufzeit wie ein Objekt behandeln zu können, wird er mit einem als Boxing bezeichneten Mechanismus in ein Objekt konvertiert. |
|